// Ensure that created/updated users have the correct credentials.
// @tags: [requires_persistence]

let mongod = MongoRunner.runMongod(
    {auth: "", setParameter: "authenticationMechanisms=SCRAM-SHA-1,SCRAM-SHA-256,PLAIN"});
const admin = mongod.getDB('admin');
const test = mongod.getDB('test');

function checkUser(userid, passwd, haveSCRAMSHA1, haveSCRAMSHA256) {
    function checkCredentialRecord(creds, hashLen, saltLen, itCount) {
        assert.eq(creds.iterationCount, itCount);
        assert.eq(creds.salt.length, saltLen);
        assert.eq(creds.storedKey.length, hashLen);
        assert.eq(creds.serverKey.length, hashLen);
    }
    function checkLogin(mech, digestOK, nodigestOK) {
        assert(test.auth({user: userid, pwd: passwd, mechanism: mech}));
        test.logout();
        assert.eq(digestOK,
                  test.auth({user: userid, pwd: passwd, mechanism: mech, digestPassword: true}));
        if (digestOK) {
            test.logout();
        }
        assert.eq(nodigestOK,
                  test.auth({user: userid, pwd: passwd, mechanism: mech, digestPassword: false}));
        if (nodigestOK) {
            test.logout();
        }
    }

    assert(admin.auth('admin', 'pass'));
    const user = admin.system.users.findOne({_id: ('test.' + userid)});
    assert.eq(user.credentials.hasOwnProperty('SCRAM-SHA-1'), haveSCRAMSHA1);
    assert.eq(user.credentials.hasOwnProperty('SCRAM-SHA-256'), haveSCRAMSHA256);

    // usersInfo contains correct mechanisms for the user
    const userInfo = assert.commandWorked(test.runCommand({usersInfo: userid}));
    assert(Array.isArray(userInfo.users[0].mechanisms));
    assert.eq(userInfo.users[0].mechanisms.includes('SCRAM-SHA-1'), haveSCRAMSHA1);
    assert.eq(userInfo.users[0].mechanisms.includes('SCRAM-SHA-256'), haveSCRAMSHA256);

    // usersInfo with showCredentials shows correct mechanisms and credentials
    const userInfoWithCredentials =
        assert.commandWorked(test.runCommand({usersInfo: userid, showCredentials: true}));
    print(tojson(userInfoWithCredentials));
    assert.eq(userInfoWithCredentials.users[0].credentials.hasOwnProperty('SCRAM-SHA-1'),
              haveSCRAMSHA1);
    assert.eq(userInfoWithCredentials.users[0].credentials.hasOwnProperty('SCRAM-SHA-256'),
              haveSCRAMSHA256);
    assert(Array.isArray(userInfoWithCredentials.users[0].mechanisms));
    assert.eq(userInfoWithCredentials.users[0].mechanisms.includes('SCRAM-SHA-1'), haveSCRAMSHA1);
    assert.eq(userInfoWithCredentials.users[0].mechanisms.includes('SCRAM-SHA-256'),
              haveSCRAMSHA256);
    admin.logout();

    if (haveSCRAMSHA1) {
        checkCredentialRecord(user.credentials['SCRAM-SHA-1'], 28, 24, 10000);
        checkLogin('SCRAM-SHA-1', true, false);
        checkLogin('PLAIN', false, true);
    }
    if (haveSCRAMSHA256) {
        checkCredentialRecord(user.credentials['SCRAM-SHA-256'], 44, 40, 15000);
        checkLogin('SCRAM-SHA-256', false, true);
        checkLogin('PLAIN', false, true);
    }
}

admin.createUser({user: 'admin', pwd: 'pass', roles: jsTest.adminUserRoles});
assert(admin.auth('admin', 'pass'));

function createUser(db, user) {
    assert(admin.auth('admin', 'pass'));
    db.createUser(user);
    admin.logout();
}

function createUserThrows(db, user) {
    assert(admin.auth('admin', 'pass'));
    assert.throws(() => db.createUser(user));
    admin.logout();
}

// Unknown mechanism.
createUserThrows(test, {
    user: 'shalala',
    pwd: 'pass',
    roles: jsTest.basicUserRoles,
    mechanisms: ['SCRAM-SHA-1', 'SCRAM-SHA-LA-LA'],
});

// By default, users are created with both SCRAM variants.
createUser(test, {user: 'user', pwd: 'pass', roles: jsTest.basicUserRoles});
checkUser('user', 'pass', true, true);

// Request SHA1 only.
createUser(
    test,
    {user: 'sha1user', pwd: 'pass', roles: jsTest.basicUserRoles, mechanisms: ['SCRAM-SHA-1']});
checkUser('sha1user', 'pass', true, false);

// Request SHA256 only.
createUser(
    test,
    {user: 'sha256user', pwd: 'pass', roles: jsTest.basicUserRoles, mechanisms: ['SCRAM-SHA-256']});
checkUser('sha256user', 'pass', false, true);

// Fail passing an empty mechanisms field.
createUserThrows(test,
                 {user: 'userNoMech', pwd: 'pass', roles: jsTest.basicUserRoles, mechanisms: []});

// Repeat above, but request client-side digesting.
// Only the SCRAM-SHA-1 exclusive version should succeed.

createUserThrows(
    test, {user: 'user2', pwd: 'pass', roles: jsTest.basicUserRoles, passwordDisgestor: 'client'});

createUser(test, {
    user: 'sha1user2',
    pwd: 'pass',
    roles: jsTest.basicUserRoles,
    mechanisms: ['SCRAM-SHA-1'],
    passwordDigestor: 'client'
});
checkUser('sha1user2', 'pass', true, false);

createUserThrows(test, {
    user: 'sha256user2',
    pwd: 'pass',
    roles: jsTest.basicUserRoles,
    mechanisms: ['SCRAM-SHA-256'],
    passwordDigestor: 'client'
});

function updateUser(db, user, props) {
    assert(admin.auth('admin', 'pass'));
    db.updateUser(user, props);
    admin.logout();
}

function updateUserThrows(db, user, props) {
    assert(admin.auth('admin', 'pass'));
    assert.throws(() => db.updateUser(user, props));
    admin.logout();
}

// Update original 1/256 user to just sha-1.
updateUser(test, 'user', {pwd: 'pass1', mechanisms: ['SCRAM-SHA-1']});
checkUser('user', 'pass1', true, false);

// Then flip to 256-only
updateUser(test, 'user', {pwd: 'pass256', mechanisms: ['SCRAM-SHA-256']});
checkUser('user', 'pass256', false, true);

// And back to (default) all.
updateUser(test, 'user', {pwd: 'passAll'});
checkUser('user', 'passAll', true, true);

// Trim out mechanisms without changing password.
updateUser(test, 'user', {mechanisms: ['SCRAM-SHA-256']});
checkUser('user', 'passAll', false, true);

// Fail when mechanisms is not a subset of the current user.
updateUserThrows(test, 'user', {mechanisms: ['SCRAM-SHA-1']});

// Fail when passing an empty mechanisms field.
updateUserThrows(test, 'user', {pwd: 'passEmpty', mechanisms: []});

// Succeed if we're using SHA-1 only.
createUser(
    test, {user: "\u2168", pwd: 'pass', roles: jsTest.basicUserRoles, mechanisms: ['SCRAM-SHA-1']});
checkUser("\u2168", 'pass', true, false);

assert(admin.auth('admin', 'pass'));

// Demonstrate that usersInfo returns all users with mechanisms lists
const allUsersInfo = assert.commandWorked(test.runCommand({usersInfo: 1}));
allUsersInfo.users.forEach(function(userObj) {
    assert(Array.isArray(userObj.mechanisms));
});

// Demonstrate that usersInfo can return all users with credentials
const allUsersInfoWithCredentials =
    assert.commandWorked(test.runCommand({usersInfo: 1, showCredentials: true}));
allUsersInfoWithCredentials.users.forEach(function(userObj) {
    assert(userObj.credentials !== undefined);
    assert(!Array.isArray(userObj.credentials));
    assert(userObj.mechanisms !== undefined);
    assert(Array.isArray(userObj.mechanisms));
});

// Demonstrate that usersInfo can find SCRAM-SHA-1 users
const allSCRAMSHA1UsersInfo =
    assert.commandWorked(test.runCommand({usersInfo: 1, filter: {mechanisms: "SCRAM-SHA-1"}}));
let foundUsers = [];
allSCRAMSHA1UsersInfo.users.forEach(function(userObj) {
    foundUsers.push(userObj.user);
});
assert.eq(["sha1user", "sha1user2", "\u2168"], foundUsers);

// Demonstrate that usersInfo can find SCRAM-SHA-256 users
const allSCRAMSHA256UsersInfo =
    assert.commandWorked(test.runCommand({usersInfo: 1, filter: {mechanisms: "SCRAM-SHA-256"}}));
foundUsers = [];
allSCRAMSHA256UsersInfo.users.forEach(function(userObj) {
    foundUsers.push(userObj.user);
});
assert.eq(["sha256user", "user"], foundUsers);

MongoRunner.stopMongod(mongod);

// Ensure mechanisms can be enabled and disabled.
function restartWithOneMech(mech) {
    const sha1ok = (mech === 'SCRAM-SHA-1');
    const sha256ok = (mech === 'SCRAM-SHA-256');
    mongod = MongoRunner.runMongod({
        auth: "",
        setParameter: "authenticationMechanisms=" + mech,
        restart: mongod,
        noCleanData: true
    });

    const test = mongod.getDB('test');
    assert.eq(test.auth("sha1user", "pass"), sha1ok);
    if (sha1ok) {
        test.logout();
    }
    assert.eq(test.auth("sha256user", "pass"), sha256ok);
    if (sha256ok) {
        test.logout();
    }
    assert(mongod.getDB('admin').auth('admin', 'pass'));
    MongoRunner.stopMongod(mongod);
}

restartWithOneMech('SCRAM-SHA-1');
restartWithOneMech('SCRAM-SHA-256');